package com.shisan.tingshu.search.runnable;

import com.shisan.tingshu.search.factory.ScheduleTaskThreadPoolFactory;
import com.shisan.tingshu.search.service.impl.ItemServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 专门用于定时重建布隆过滤器的线程任务类
 */
@Slf4j
public class RebuildBloomFilterRunnable implements Runnable {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    private RedissonClient redissonClient;

    private StringRedisTemplate redisTemplate;

    private ItemServiceImpl itemServiceImpl;

    public RebuildBloomFilterRunnable(RedissonClient redissonClient, StringRedisTemplate redisTemplate, ItemServiceImpl itemServiceImpl) {

        this.redissonClient = redissonClient;
        this.redisTemplate = redisTemplate;
        this.itemServiceImpl = itemServiceImpl;
    }


    @Override
    public void run() {
        // 步骤：删除老布隆的数据 >> 删除老布隆的配置 >> 创建新布隆 >> 初始化新布隆 >> 将数据放到新布隆
        // 但在高并发场景下，第一个线程删除了老布隆的配置但是新布隆还没有创建时，第二个线程进来仍然使用的是老布隆，此时就会报错
        // 优化做法：创建新布隆 >> 初始化新布隆 >> 将数据放到新布隆 >> 删除老布隆的数据 >> 删除老布隆的配置 >> 将新布隆的名字重命名为老布隆的名字（第4、5、6步要做成一个原子操作）

        // 1、创建新布隆
        RBloomFilter<Object> albumIdBloomFilterNew = redissonClient.getBloomFilter("albumIdBloomFilterNew");
        // 2、初始化新布隆
        albumIdBloomFilterNew.tryInit(1000000l, 0.001);
        // 3、将数据放到新布隆
        List<Long> albumInfoIdList = itemServiceImpl.getAlbumInfoIdList();
        for (Long albumId : albumInfoIdList) {
            albumIdBloomFilterNew.add(albumId);
        }
        albumIdBloomFilterNew.add(2000L); // 给新布隆添加一个老布隆不存在的数据，用于测试
        // 用lua脚本保证这三个步骤的原子性：4、删除老布隆的数据；5、删除老布隆的配置；6、将新布隆的名字重命名为老布隆的名字
        String script = " redis.call(\"del\",KEYS[1])" +
                "  redis.call(\"del\",KEYS[2])" +
                // KEYS[1]对应的是下面asList的第一个元素，KEYS[2]对应的是下面asList的第二个元素，以此类推
                "  redis.call(\"rename\",KEYS[3],KEYS[1])" + // 用后者替换前者
                "  redis.call(\"rename\",KEYS[4],KEYS[2]) return 0";
        List<String> asList = Arrays.asList("albumIdBloomFilter", "{albumIdBloomFilter}:config", "albumIdBloomFilterNew", "{albumIdBloomFilterNew}:config");
        Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), asList);
        if (execute == 0) {
            log.info("老布隆被删除，新布隆上线");
        }

        // 一次性延迟任务+嵌套任务本身，进而实现定时的效果（Nacos源码就是这么做的）
//        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//        scheduledExecutorService.schedule(this,10, TimeUnit.SECONDS);

        //但是定时任务线程池被创建了两次（ItemServiceImpl的initRebuildBloomFilter()中一次，上面一次），所以可以使用工厂模式
        ScheduleTaskThreadPoolFactory instance = ScheduleTaskThreadPoolFactory.getINSTANCE();
        instance.execute(this, 7l, TimeUnit.DAYS);
//        instance.execute(this, 20l, TimeUnit.SECONDS); // 测试用
    }
}
